package com.lifeonwalden.dsg.service;

import com.lifeonwalden.dsg.bean.DateGenerateRule;

import java.time.*;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 内部工具类
 * <p>
 * 请勿直接使用
 */
public interface InnerMonthFrequencyUtil {
    static List<Date> generate(DateGenerateRule rule, DateService dateService) {
        List<Date> dateList = new ArrayList<>();

        if (rule.getWeekOf().isPresent()) {
            if (!rule.getDayOf().isPresent()) {
                throw new RuntimeException("Need dayOf for generating date series of week frequency.");
            }

            if (rule.getWeekOf().get() == 0) {
                throw new RuntimeException("The weekOf should not be zero.");
            }

            Instant end = rule.getEnd().toInstant();
            Instant date;

            int weekOf = rule.getWeekOf().get();
            int dayOf = rule.getDayOf().get();

            if (rule.isFocusOnDayOfWeek()) {
                if (weekOf > 0) {
                    ZonedDateTime firstDateOfMonth = locateDate(rule.getStart().toInstant().atZone(ZoneId.systemDefault()), 1);
                    do {
                        ZonedDateTime firstWeekDay = InnerWeekFrequencyUtil.locateDate(firstDateOfMonth.toInstant(), DayOfWeek.of(dayOf)).atZone(ZoneId.systemDefault());
                        if (firstWeekDay.compareTo(firstDateOfMonth) >= 0) {
                            date = weekOf > 1 ? firstWeekDay.plusDays((weekOf - 1) * 7).toInstant() : firstWeekDay.toInstant();
                        } else {
                            date = firstWeekDay.plusDays(weekOf * 7).toInstant();
                        }
                        InnerUtil.acceptValidDate(dateList, Date.from(date), rule, dateService);

                        firstDateOfMonth = firstDateOfMonth.plusMonths(rule.getStep());
                    } while (date.compareTo(end) <= 0);
                } else {
                    ZonedDateTime lastDateOfMonth = locateDate(rule.getStart().toInstant().atZone(ZoneId.systemDefault()), -1);
                    do {
                        ZonedDateTime lastWeekDay = InnerWeekFrequencyUtil.locateDate(lastDateOfMonth.toInstant(), DayOfWeek.of(dayOf)).atZone(ZoneId.systemDefault());
                        if (lastWeekDay.compareTo(lastDateOfMonth) <= 0) {
                            date = weekOf < -1 ? lastWeekDay.plusDays((weekOf + 1) * 7).toInstant() : lastWeekDay.toInstant();
                        } else {
                            date = lastWeekDay.plusDays(weekOf * 7).toInstant();
                        }
                        InnerUtil.acceptValidDate(dateList, Date.from(date), rule, dateService);

                        lastDateOfMonth = lastDateOfMonth.plusMonths(rule.getStep());
                        lastDateOfMonth = locateDate(lastDateOfMonth, -1);
                    } while (date.compareTo(end) <= 0);
                }
            } else {
                if (weekOf > 0) {
                    ZonedDateTime firstDateOfMonth = locateDate(rule.getStart().toInstant().atZone(ZoneId.systemDefault()), 1);
                    do {
                        Instant firstWeekDay = InnerWeekFrequencyUtil.locateDate(firstDateOfMonth.toInstant(), DayOfWeek.of(dayOf));
                        date = weekOf > 1 ? firstWeekDay.plus((weekOf - 1) * 7, ChronoUnit.DAYS) : firstWeekDay;
                        if (date.atZone(ZoneId.systemDefault()).getMonth() == firstDateOfMonth.getMonth()) {
                            InnerUtil.acceptValidDate(dateList, Date.from(date), rule, dateService);
                        }

                        firstDateOfMonth = firstDateOfMonth.plusMonths(rule.getStep());
                    } while (date.compareTo(end) <= 0);
                } else {
                    ZonedDateTime lastDateOfMonth = locateDate(rule.getStart().toInstant().atZone(ZoneId.systemDefault()), -1);
                    do {
                        ZonedDateTime lastWeekDay = InnerWeekFrequencyUtil.locateDate(lastDateOfMonth.toInstant(), DayOfWeek.of(dayOf)).atZone(ZoneId.systemDefault());
                        date = weekOf < -1 ? lastWeekDay.plusDays((weekOf + 1) * 7).toInstant() : lastWeekDay.toInstant();
                        if (date.atZone(ZoneId.systemDefault()).getMonth() == lastDateOfMonth.getMonth()) {
                            InnerUtil.acceptValidDate(dateList, Date.from(date), rule, dateService);
                        }

                        lastDateOfMonth = lastDateOfMonth.plusMonths(rule.getStep());
                        lastDateOfMonth = locateDate(lastDateOfMonth, -1);
                    } while (date.compareTo(end) <= 0);
                }
            }
        } else {
            if (!rule.getDayOf().isPresent()) {
                throw new RuntimeException("Need dayOf for generating date series of month frequency.");
            }

            ZonedDateTime date = locateDate(rule.getStart().toInstant().atZone(ZoneId.systemDefault()), rule.getDayOf().get());
            ZonedDateTime end = rule.getEnd().toInstant().atZone(ZoneId.systemDefault());
            do {
                InnerUtil.acceptValidDate(dateList, Date.from(date.toInstant()), rule, dateService);
                date = date.plusMonths(rule.getStep());
                date = locateDate(date, rule.getDayOf().get());
            } while (date.compareTo(end) <= 0);
        }

        return dateList;
    }

    static ZonedDateTime locateDate(ZonedDateTime startDate, int dayOfMonth) {
        if (dayOfMonth > 0) {
            return startDate.with(MonthDay.of(startDate.getMonth(), dayOfMonth));
        } else if (dayOfMonth < 0) {
            ZonedDateTime date = startDate.plusMonths(1);
            return date.with(MonthDay.of(date.getMonth(), 1)).plus(dayOfMonth, ChronoUnit.DAYS);
        } else {
            throw new RuntimeException("The dayOf should not be zero.");
        }
    }
}
